/*
 * Copyright (c) 2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.libermundi.theorcs.core.services.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceUnit;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.search.Query;
import org.hibernate.CacheMode;
import org.hibernate.search.ProjectionConstants;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.hibernate.search.query.dsl.TermMatchingContext;
import org.libermundi.theorcs.core.model.Searchable;
import org.libermundi.theorcs.core.model.base.NumericIdEntity;
import org.libermundi.theorcs.core.services.GenericManager;
import org.libermundi.theorcs.core.services.SearchServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

/**
 * @author Martin Papy
 *
 */
@Service("SearchServices")
@Transactional
public class HibernateSearchServicesImpl implements SearchServices {
	private static final Logger logger = LoggerFactory.getLogger(HibernateSearchServicesImpl.class);
	
	private Map<Class<?>,String[]> _entities = Maps.newHashMap(); 
	
    private FullTextEntityManager _entityManager;
    
	@Autowired
    private GenericManager _genericManager;
    
    public HibernateSearchServicesImpl() {
		super();
	}

    /**
     * Set entity manager.
     *
     * @param entityManager entity manager
     */
    @PersistenceUnit
    public final void setEntityManager(EntityManagerFactory entityManagerFactory) {
        _entityManager = Search.getFullTextEntityManager(entityManagerFactory.createEntityManager());
    }
	
    public List<String> getSuggestions(String searchTerm) {
    	List<String> suggestions = Lists.newArrayList();
    	for (Entry<Class<?>, String[]> entityEntry : _entities.entrySet()) {
        	Class<?> entity = entityEntry.getKey();
         	suggestions.addAll(getSuggestions(searchTerm, entity));			
		}
    	return suggestions;
    }
    
	public List<String> getSuggestions(String searchTerm, Class<?> entity) {
		if(logger.isDebugEnabled()){
			logger.debug("Searching Suggestions for term [{}] and Entity [{}]",searchTerm, entity);
		}
	    // Keep a list of suggestions retrieved by search over all fields
	    List<String> suggestions = Lists.newArrayList();
		
		// Compose query for term over all fields in our Domain objects
	    String lowerCasedSearchTerm = searchTerm.toLowerCase();

	    // New DSL based query composition
	    String[] searchfields = getSearchFields(entity);
	    
	    if(searchfields == null) { // Apparently no Entity exist in the DB. Just skip this one.
	    	return suggestions;
	    }
	    
	    TermMatchingContext onFields = getQueryBuilder(entity)
				.keyword()
				.wildcard()
				.onField(searchfields[0]);
	    
	    for (int i = 1; i < searchfields.length; i++)
	        onFields.andField(searchfields[i]);
	    
	    Query query = onFields.matching(searchTerm.toLowerCase())
	    				.createQuery();

	    // Convert the Search Query into something that provides results: Specify "Entity" again to be future proof
	    FullTextQuery fullTextQuery = _entityManager.createFullTextQuery(query, entity);
	    	fullTextQuery.setMaxResults(20);

	    // Projection does not work on collections or maps which are indexed via @IndexedEmbedded
	    List<String> projectedFields = Lists.newArrayList();
	    	projectedFields.add(ProjectionConstants.DOCUMENT);
	    
	    List<String> embeddedFields = new ArrayList<String>();
	    for (String fieldName : searchfields)
	        if (fieldName.contains("."))
	            embeddedFields.add(fieldName);
	        else
	            projectedFields.add(fieldName);

	    @SuppressWarnings("unchecked")
	    List<Object[]> results = fullTextQuery.setProjection(projectedFields.toArray(new String[projectedFields.size()])).getResultList();

	    for (Object[] projectedObjects : results) {
	        // Retrieve the search suggestions for the simple projected field values
	        for (int i = 1; i < projectedObjects.length; i++) {
	        	if(projectedObjects[i] != null){
		            String fieldValue = projectedObjects[i].toString();
		            if (fieldValue.toLowerCase().contains(lowerCasedSearchTerm))
		                suggestions.add(fieldValue);
	        	}
	        }

	        // Extract the search suggestions for the embedded fields from the document
	        Document document = (Document) projectedObjects[0];
	        for (String fieldName : embeddedFields)
	            for (Fieldable field : document.getFieldables(fieldName))
	                if (field.stringValue().toLowerCase().contains(lowerCasedSearchTerm))
	                    suggestions.add(field.stringValue());
	    }
	    if(logger.isDebugEnabled()){
			logger.debug("Found {} match(es)",suggestions.size());
		}

	    // Return the composed list of suggestions, which might be empty
	    return suggestions;
	}
	
	/**
	 * @param entity
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private String[] getSearchFields(Class<?> entity) {
		if(_entities.get(entity) == null) {
			Searchable exemple = (Searchable)_genericManager.getLast((Class<NumericIdEntity>)entity);
			if(exemple !=null){
				_entities.put(entity,exemple.loadSearchfields());
			}
		}
		return _entities.get(entity);
	}

	private QueryBuilder getQueryBuilder(Class<?> clazz) {
		return _entityManager.getSearchFactory().buildQueryBuilder().forEntity(clazz).get();
	}

	/* (non-Javadoc)
	 * @see org.libermundi.theorcs.core.services.SearchServices#addSearchableEntity(java.lang.Class)
	 */
	@Override
	public void addSearchableEntity(Class<? extends Searchable> entity) {
		if(logger.isDebugEnabled()) {
			logger.debug("Adding Searchable Entity [{}]",entity);
		}
		_entities.put(entity,null); // We will add the searchFields later when the APp is started already. That avoid some issues at startup.
	}

	/* (non-Javadoc)
	 * @see org.libermundi.theorcs.core.services.SearchServices#addSearchableEntities(java.util.List)
	 */
	@Override
	public void addSearchableEntities(List<Class<? extends Searchable>> entities) {
		for (Class<? extends Searchable> entity : entities) {
			addSearchableEntity(entity);		
		}
	}

	/* (non-Javadoc)
	 * @see org.libermundi.theorcs.core.services.SearchServices#doFullindex()
	 */
	@Override
	public void doFullindex() {
		//TODO Switch the application to Maintenance mode
        for (Entry<Class<?>, String[]> entityEntry : _entities.entrySet()) {
        	Class<?> entity = entityEntry.getKey();
            if(logger.isInfoEnabled()){
            	logger.info("Rebuild Index for Entity [{}]",entity);
            }
        	try {
        		_entityManager
					 .createIndexer( entity )
					 .batchSizeToLoadObjects( 25 )
					 .cacheMode( CacheMode.IGNORE )
					 .threadsToLoadObjects( 5 )
					 .threadsForSubsequentFetching( 20 )
					 .startAndWait();
			} catch (InterruptedException e) {
            	logger.error("Operation Interupted while Rebuilding the Index for Entity ["+entity+"]",e);
			}
		}
	}
}
